在上一篇文章中,我們介紹了 PostgreSQL 的隔離層級(Isolation Levels),了解資料庫如何在多個交易同時進行時維持一致性。但實務上,光靠隔離層級並不足以避免所有的衝突與問題,這時就需要更細緻的控制方式:鎖(Lock)。
鎖的目的,是在多個交易同時讀寫資料時,防止資料不一致、競爭或衝突的情況發生。依照鎖的範圍,可以分成 Table Lock 以及 Row Lock:
這篇文章會先從 Table Lock 開始,說明各種不同類型的鎖、它們在什麼時候會被觸發,以及如何觀察與測試它們。明天文章會再深入探討 Row Lock,了解它們的使用時機跟用法。
以 Table Lock 來說,在 PostgreSQL 中每次對資料表的存取(SELECT、INSERT、UPDATE、DELETE...)都會取得一種「鎖」,用來保證資料一致性與避免衝突。Table Lock 總共有 8 種,下面的表格是根據文件,整理出來的八種 Lock 出現的時機:
Lock Mode | Commands |
---|---|
ACCESS SHARE | SELECT |
ROW SHARE | SELECT FOR UPDATE , SELECT FOR NO KEY UPDATE , SELECT FOR SHARE , SELECT FOR KEY SHARE |
ROW EXCLUSIVE | UPDATE , DELETE , INSERT , MERGE |
SHARE UPDATE EXCLUSIVE | VACUUM , ANALYZE , CREATE INDEX CONCURRENTLY , CREATE STATISTICS , COMMENT ON |
SHARE | CREATE INDEX |
SHARE ROW EXCLUSIVE | CREATE TRIGGER |
EXCLUSIVE | REFRESH MATERIALIZED VIEW CONCURRENTLY |
ACCESS EXCLUSIVE | DROP TABLE , TRUNCATE , REINDEX , VACUUM FULL , REFRESH MATERIALIZED VIEW , ALTER TABLE |
這 8 種 Table Lock 又各自與其他鎖可能互斥,這是 PostgreSQL 官方文件整理出來的 Table Lock 互斥的表,等下的實驗可以再回來看對照表格一起看。
https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES
接下來我想要做兩個實驗:
ACCESS EXCLUSIVE
是層級最大的鎖,它和每一個其他鎖都互斥,看起來好像只要做跟改動 table schema 相關的操作就會拿到這個鎖,我想試試看是不是真的其他操作都沒辦法做。CREATE INDEX CONCURRENTLY
,它可以讓建立 Index 時還能修改資料,我想試試看它拿到的 SHARE UPDATE EXCLUSIVE
鎖,是不是真的開放資料修改。這是 PostgreSQL Table Lock 最嚴格的表鎖,只要執行 ALTER
、DROP
、TRUNCATE
等操作時會觸發,會阻擋所有其他存取。
CREATE TABLE lock_test (
id SERIAL PRIMARY KEY,
value TEXT
);
INSERT INTO lock_test (value) VALUES ('a'), ('b'), ('c');
ACCESS EXCLUSIVE
lockBEGIN;
ALTER TABLE lock_test ADD COLUMN dummy TEXT;
-- DO NOT COMMIT YET!
可以看到等了 10 秒還是沒有回覆,因為被 Session A 的 ACCESS EXCLUSIVE
鎖擋住。
SELECT * FROM lock_test;
這時候可以利用 pg_lock
查到 lock_test
目前有的 lock。
SELECT pid, mode, relation::regclass, granted
FROM pg_locks
WHERE relation::regclass::text = 'lock_test';
SELECT
被擋住是因為要拿 ACCESS SHARE
的鎖,但是它與 ACCESS EXCLUSIVE
兩者衝突(可以參考表格)。這時候只要再 COMMIT
把 transaction 結束之後,ACCESS EXCLUSIVE
的鎖釋放,就可以正常 SELECT
了,也會看到新增的 dummy 欄位。
COMMIT;
SELECT * FROM lock_test;
SHARE UPDATE EXCLUSIVE
真的可以讓 table 開放修改資料嗎?根據文件 CREATE INDEX CONCURRENTLY
是拿到這個鎖,我們可以用 LOCK
指令來模擬這件事。
CREATE INDEX CONCURRENTLY
,把 table 鎖住,試試看更改裡面的資料Session A - 鎖表
BEGIN;
LOCK TABLE lock_test IN SHARE UPDATE EXCLUSIVE MODE;
Session B - 嘗試更改資料
BEGIN;
UPDATE lock_test SET value = 'd' WHERE id = 1;
UPDATE
成功了,那如果是用 CREATE INDEX
的 SHARE
鎖,也可以做一樣的事嗎?記得先把上面這兩個 transaction COMMIT
或 ROLLBACK
再繼續往下。
CREATE INDEX
,把 table 鎖住,試試看更改裡面的資料Session A - 鎖表
BEGIN;
LOCK TABLE lock_test IN SHARE MODE;
Session B - 嘗試修改
BEGIN;
UPDATE lock_test SET value = 'e' WHERE id = 1;
發現這個 UPDATE
真的就卡住了,沒辦法更改。這時候再回去 Session A COMMIT;
,代表 Session A 把鎖釋放,再看 Session B 就會發現成功更改了,但是他花了 36 秒的時間,因為剛剛一直在等 Session A 的鎖。
有時候不知道為什麼卡住的時候,可以利用 pg_stat_activity
以及 pg_blocking_pids
這兩張 table 來幫忙。
在 pg_stat_activity
中有一個 state
欄位,如果像剛剛那樣執行一次最後一個實驗,查詢時就可以找到 idle in transaction 的 狀態,並且後面可以看到相對的 query
。
除了 state
以外,也有 xact_start
可以得知 transaction 開始的時間,以及這個操作的 pid
。
而 pg_blocking_pids
是用來查哪一個操作擋住我,比如我可以先從 pg_stat_activity
找到 UPDATE
指令 的 pid
是 84663,我就可以用這個 84663 去找哪一個 pid
擋住我。
pg_blocking_pids
會回傳一個陣列,裡面是所有擋住這個操作的 pids。像是我找到是 83934 擋住,就可以再回去 pg_stat_activity
看是哪一個 query 。也可以兩張表 join 之後選擇想要看的欄位,就會很清楚知道資料庫目前發生什麼事情了!
SELECT
a.pid, -- 目前操作
a.usename,
a.state,
a.query,
age(now(), a.query_start) AS query_age, -- 計算已執行多久
b.pid AS blocking_pid, -- 被誰擋住
b.usename AS blocking_user,
b.query AS blocking_query -- 擋住的 query 是什麼
FROM pg_stat_activity a
JOIN pg_stat_activity b ON b.pid = ANY(pg_blocking_pids(a.pid))
不過雖然 Table Lock 能保護整張資料表的完整性,但在實務上,如果每一個鎖都要鎖到一整張表,非常容易造成等待與效能瓶頸。因此也會需要使用到 Row Lock,只鎖定實際被操作的 row,避免不必要的阻塞。
明天我們就來看看 Row Level Lock 有哪些,那就明天見了~
pg_blocking_pids
以及 pg_stat_activity
這兩張 table 來查詢。https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-TABLES
https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW
https://www.postgresql.org/docs/9.6/functions-info.html